MDI stands for Multiple Document Interface and is the type of user interface used by most of the applications in the Microsoft Office suite, including Microsoft Word, Microsoft Excel, and Microsoft PowerPoint. Many applications lend themselves to implementation via an MDI user interface. Whenever you have an application that should be able to deal with multiple documents at the same time, an MDI interface is probably the best choice.
Building Visual Basic MDI applications is simple, as long as you know how to make the best use of a few features of the language. You begin developing an MDI application by adding an MDIForm module to the current project. An MDIForm module is similar to a regular Form module, with just a few peculiarities:
An MDIForm object contains one or more child forms. To create such child forms, you add a regular form to the project and set its MDIChild property to True. When you do this, the form's icon in the Project Explorer window changes, as shown in Figure 9-11. You don't have to specify which MDI form this form is a child of because there can be only one MDIForm module per project.
An MDI child form can't be displayed outside its parent MDIForm. If an MDI child form is the startup form for an application, its parent MDI form is automatically loaded and displayed before the child form becomes visible. Apart from the startup form, all instances of the MDI child form are created using the New keyword:
' Inside the MDIForm module Private Sub mnuFileNew_Click() Dim frmDoc As New frmDocument frmDoc.Show End Sub |
MDIForm modules support an additional property, AutoShowChildren. When this property is True (the default value), an MDI child form is displayed inside its parent MDI form as soon as you load the parent. In other words, you can't load an MDI child form and keep it hidden unless you set this property to False.
MDI child forms have other peculiarities as well. For example, they don't display menu bars as regular forms do: If you add one or more top-level menus to an MDI child form, when the form becomes active its menu bar replaces the MDI parent form's menu bar. For this reason, it's customary for MDI child forms not to include a menu; you define menus only for the main MDIForm module.
Figure 9-11. The MDI Notepad application at design time.
When a menu command is invoked in the MDIForm module, you normally apply it to the MDI child form that's currently active, which you do through the ActiveForm property. For example, here's how you execute the Close command on the File menu:
' In the MDI parent form Private Sub mnuFileClose_Click() ' Close the active form, if there is one. If Not (ActiveForm Is Nothing) Then Unload ActiveForm End Sub |
You should always check for an ActiveForm because it's possible that no MDI child form is currently open, in which case ActiveForm returns Nothing. (It doesn't return a reference to the MDIForm itself, as you might expect.) If your MDI application supports different kinds of child forms, you often need to figure out which form is the active form, as in the code below.
Private Sub mnuFilePrint_Click() If TypeOf ActiveForm Is frmDocument Then ' Print the contents of a TextBox control. Printer.Print ActiveForm.txtEditor.Text Printer.EndDoc ElseIf TypeOf ActiveForm Is frmImageViewer Then ' Print the contents of a PictureBox control. Printer.PaintPicture ActiveForm.picImage.Picture, 0, 0 Printer.EndDoc End If End Sub |
MDIForm modules support an additional method that's not exposed by regular forms: the Arrange method. This method provides a quick way to programmatically arrange all the child forms in an MDI application. You can tile all child forms horizontally or vertically, you can arrange them in a cascading fashion, or you can line up all the minimized forms in an orderly fashion near the bottom of the MDI parent form. To this purpose, you usually create a Window menu with four commands: Tile Horizontally, Tile Vertically, Cascade, and Arrange Icons. This is the code behind these menu items:
Private Sub mnuTileHorizontally_Click() Arrange vbTileHorizontal End Sub Private Sub mnuTileVertically_Click() Arrange vbTileVertical End Sub Private Sub mnuCascade_Click() Arrange vbCascade End Sub Private Sub mnuArrangeIcons_Click() Arrange vbArrangeIcons End Sub |
It's also customary for the Window menu to include a list of all open MDI child forms and to let the user quickly switch to any one of them with a click of the mouse. (See Figure 9-12.) Visual Basic makes it simple to add this feature to your MDI applications: You only have to tick the WindowList option in the Menu Editor for the top-level Window menu. Alternatively, you can create a submenu with the list of all open windows by ticking the WindowList option for a lower level menu item. In any case, only one menu item can have this option ticked.
Figure 9-12. The Window menu lets you tile and arrange all MDI child windows and quickly switch to any one of them with a click of the mouse.
In Visual Basic 3, writing MDI applications wasn't particularly easy because you had to keep track of the status of each MDI child form using an array of UDTs, and it was up to you to update this array whenever an MDI child form was created or closed. Things are much simpler in Visual Basic 4 and later versions because each form can support custom properties and you can store the data right in MDI child form modules without the need for a global array of UDTs.
Typically, all MDI child forms support at least two custom properties, Filename and IsDirty (of course, actual names can be different). The Filename property stores the name of the data file from where data was loaded, whereas IsDirty is a Boolean flag that tells whether data was modified by the user. Here's how these properties are implemented in the MDI Notepad sample program:
' Inside the frmDocument MDI child form Public IsDirty As Boolean Private m_FileName As String Property Get Filename() As String Filename = m_FileName End Property Property Let Filename(ByVal newValue As String) m_FileName = newValue ' Show the filename on the form's Caption. Caption = IIf(newValue = "", "Untitled", newValue) End Property Private Sub txtEditor_Change() IsDirty = True End Sub |
You need the IsDirty property so that you can ask the user if he or she wants to save modified data when closing the form. This is done in the MDI child form's Unload event procedure:
Private Sub Form_Unload(Cancel As Integer) Dim answer As Integer If IsDirty Then answer = MsgBox("This document has been modified. " & vbCr _ & "Do you want to save it?", vbYesNoCancel + vbInformation) Select Case answer Case vbNo ' The form will unload without saving data. Case vbYes ' Delegate to a procedure in the main MDI form. frmMainMDI.SaveToFile Filename Case vbCancel ' Refuse to unload the form. Cancel = True End Select End If End Sub |
The MDI Notepad application described in the previous section is perfectly functional, but it can't be regarded as a good example of object-oriented design. In fact, the MDIForm object breaks the encapsulation of MDI child forms because it directly accesses the properties of the txtEditor control. This might appear to be a minor defect, but you know that good encapsulation is the key to reusable, easily maintainable, bug-free software. I'll demonstrate this concept by offering an alternate way to design an MDI application.
If you don't want the parent MDI form to directly access controls on its child forms, the solution is to define an interface through which the two forms can talk to one another. For example, instead of loading and saving text by manipulating the txtEditor's properties, the parent MDI form should ask the child form to load or save a given file. Similarly, instead of directly cutting, copying, and pasting data on the txtEditor control, the parent MDI form should invoke a method in the child form that does the job. The parent MDI form should also query the MDI child form to learn which commands should be made available in the Edit menu.
After playing for a while with MDI projects, I came up with a simple interface that's generic enough to fit many MDI applications. In addition to the usual Filename and IsDirty properties, this interface includes properties such as IsEmpty (True if the MDI child form doesn't contain any data), CanSave, CanCut, CanCopy, CanPaste, and CanPrint, as well as methods such as Cut, Copy, Paste, PrintDoc, LoadFile, SaveFile, and AskFilename (which uses a FileOpen or FileSave common dialog). This interface permits you to rewrite the MDI Notepad application without breaking the encapsulation of MDI child forms. For example, this is the code that implements the Save As command on the File menu in the MDI parent form:
Private Sub mnuFileSaveAs_Click() ' Ask the document to show a common dialog, and ' then save the file with the name selected by the user. On Error Resume Next ActiveForm.SaveFile ActiveForm.AskFilename(True) End Sub |
And this is how the MDI child form implements the PrintDoc method:
Sub PrintDoc() Printer.NewPage Printer.Print txtEditor.Text Printer.EndDoc End Sub |
As usual, the complete source code of this new version of the application is available on the companion CD. You'll notice that the overall amount of code is slightly larger than the original MDI Notepad application. But this new structure has several benefits, which will be apparent in a moment.
NOTE
In this sample program, I defined a set of properties and methods. Then I added them to the primary interface of the frmDocument MDI child form. Because the frmMain MDI parent form accesses all its child forms through the ActiveForm property, properties and methods of this interface are accessed through late binding, which means that you must protect each reference with an On Error statement. For a more robust implementation, define a secondary interface as an abstract class and implement it in each MDI child form module.
Because this new version of the MDI parent form never breaks the encapsulation of the MDI child forms, you're free to change the implementation of MDI child forms without affecting the rest of the application. For example, you can turn the Notepad-like program into an MDI image viewer application. In this case, the MDI child form hosts a PictureBox control, so you have to modify the implementation of all the properties and methods of the interface used for the parent-child communication. For example, the PrintDoc method is now implemented as follows:
Sub PrintDoc() Printer.NewPage Printer.PaintPicture picBitmap.Picture, 0, 0 Printer.EndDoc End Sub |
Surprisingly, you need to modify fewer than 20 lines of code to morph the MDI Notepad application into an image viewer application. But the most interesting detail is that you don't need to modify one single line of code in the frmMain module. In other words, you have created a reusable, polymorphic MDI parent form!
Alternatively, if you're willing to slightly modify the parent MDI form's code, you can have the same MDI container work for different types of child forms at the same time. Figure 9-13 shows this new version of the sample MDI application, which hosts text documents and images at the same time. You can add new types of child forms or expand the interface to take additional properties and methods into account.
Figure 9-13. You can reuse the frmMain.frm generic MDI form with different child forms, for example, mini word processors and image viewers.
Visual Basic 6 comes with a revamped Application Wizard, which is more flexible than the one provided with Visual Basic 5 and is tightly integrated with the Toolbar Wizard and the Form Wizard.
The Application Wizard is automatically installed by the Visual Basic setup procedure, so you just need to make it available in the Add-In menu by selecting it in the Add-In Manager window. When you run the wizard, you can choose from among MDI, SDI (Single Document Interface, applications based on standard forms), and Windows Explorer-like applications, as you can see in Figure 9-14.
If you select the MDI option, you're asked to configure your menus (Figure 9-15): this tool is so simple to use and so intuitive that you'll probably wish you could have it when you're working with the standard Menu Editor.
Figure 9-14. The Application Wizard: Choosing the interface.
Figure 9-15. The Application Wizard: Selecting menus.
In the next step, you configure the program's toolbar (Figure 9-16) using an embedded wizard. This slick tool is also available outside the Application Wizard, and you'll find it in the Add-In menu under the name Toolbar Wizard.
In subsequent steps, the Application Wizard asks you whether you want to use resource files and whether you want to add an item in the Help menu that points to your Web site. You can then select additional forms to be added to the project (Figure 9-17), choosing among four standard forms and any form templates you have defined previously. Finally, you can create any number of data-bound forms: in this case, the wizard calls the Data Form wizard, which I illustrated in Chapter 8. In the last step, you can decide to save all the current settings to a configuration file so that the next time you run the Application Wizard you can speed up the process even more.
Figure 9-16. The Application Wizard: Customizing the toolbar.
Figure 9-17. The Application Wizard: Selecting additional forms.
While the code delivered by the Application Wizard is a good starting point for building your own MDI application, in my opinion it leaves much to be desired. The MDI application created by the wizard uses a sample MDI child form that hosts a RichTextBox control to build a simple word processor-like application. On some occasions, however, the buttons on the toolbar don't work as they should, and the code for setting up all common dialogs isn't properly implemented, just to name a few shortcomings. Unfortunately, you have no control over the code generated by the wizard, so each time you run the wizard you must fix the resulting code by hand.